package com.qs.commontools.common.service;

import com.alibaba.fastjson.JSON;
import com.qs.commontools.common.entity.http.HttpsX509TrustManager;
import com.qs.commontools.common.exception.HttpConnectException;
import com.qs.commontools.common.exception.URLErrorException;
import com.qs.commontools.common.exception.URLNotFountException;
import com.qs.commontools.constants.HttpToolConstant;
import com.qs.commontools.utils.base.StringUtils;
import com.qs.commontools.utils.http.HttpTool;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.*;
import java.net.*;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

/**
 * http工具类
 * @author qiusuo
 * @since 2021-09-07
 */
public class HttpService {


    private static HttpService instance;

    private HttpService() {
    }

    /**
     * 方法同步，调用效率低
     * @return
     */
    public static synchronized HttpService getInstance(){
        if(instance == null){
            instance = new HttpService();
        }
        return instance;
    }

    /**
     * todo 创建一个链接对象
     * @return
     */
    public HttpURLConnection build(HttpTool httpTool) throws IOException, URLErrorException, KeyManagementException, NoSuchAlgorithmException, NoSuchProviderException {
        URL url = null;
        // url路径
        String urlStr = httpTool.getUrl();
        // get请求
        if (HttpToolConstant.REQUEST_METHOD_GET.equals(httpTool.getRequestMethod())) {
            // 如果本就包含"?"号，则判断路径包含参数
            if (urlStr.contains("?")) {
                urlStr = getUrlEncode(httpTool.getUrl());
            } else {
                // 不包含"?"
                // 判断是否设置了入参,get入参一般都是键值对，所以暂时只校验键值对
                if (null != httpTool.getParamMap()) {
                    Map<String,Object> map = httpTool.getParamMap();
                    // 将map对象入参转为&拼接的入参
                    String s = mapToStringParam(map);
                    urlStr = httpTool.getUrl() + "?" + s;
                }
            }

        }

        HttpURLConnection con ;
        url = new URL(urlStr);

        // 假如是https请求
        if (urlStr.contains(HttpToolConstant.HTTP_STR_HTTPS)) {
            // 创建SSLContext，第一个参数为 返回实现指定安全套接字协议的SSLContext对象。第二个为提供者
            SSLContext sslContext = SSLContext.getInstance("SSL","SunJSSE");
            // SSLContext sslContext = SSLContext.getInstance("TLS")
            TrustManager[] tm = { new HttpsX509TrustManager() };
            // 初始化，参考https://vimsky.com/examples/detail/java-method-javax.net.ssl.SSLContext.init.html
            sslContext.init(null, tm, new SecureRandom());
            // 获取SSLSocketFactory对象
            SSLSocketFactory ssf = sslContext.getSocketFactory();

            HttpsURLConnection httpsURLConnection = null;
            // 判断是否需要设置代理
            if (!StringUtils.isBlank(httpTool.getProxyUrl())) {
                Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(httpTool.getProxyUrl(), httpTool.getProxyPort()));
                httpsURLConnection = (HttpsURLConnection) url.openConnection(proxy);
            } else {
                httpsURLConnection = (HttpsURLConnection) url.openConnection();
            }
            httpsURLConnection.setSSLSocketFactory(ssf);
            con = httpsURLConnection;
        } else {
            // 判断是否需要设置代理
            if (!StringUtils.isBlank(httpTool.getProxyUrl())) {
                Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(httpTool.getProxyUrl(), httpTool.getProxyPort()));
                con = (HttpURLConnection) url.openConnection(proxy);
            } else {
                con = (HttpURLConnection) url.openConnection();
            }
        }

        // 如果是post
        if (HttpToolConstant.REQUEST_METHOD_POST.equals(httpTool.getRequestMethod())) {
            // 指定流的大小，当内容达到这个值的时候就把流输出
            con.setChunkedStreamingMode(HttpToolConstant.NUM_INTEGER_KB * HttpToolConstant.NUM_INTEGER_TEN);
        }
        return con;
    }

    /**
     * todo 校验 http实体
     * @param httpTool http实体
     */
    public void check(HttpTool httpTool) throws URLNotFountException{
        // 是否传入了url
        if (StringUtils.isBlank(httpTool.getUrl())) {
            throw new URLNotFountException("url参数不能为空字符串或者为null，后端调用HttpEntity实例方法setUrl()进行设置");
        }
    }


    /**
     * todo 设置请求头
     * @param httpURLConnection http连接对象
     * @param header 请求头
     * @return 设置请求头之后的http连接对象
     */
    public void setHeader(HttpURLConnection httpURLConnection,Map<String,String> header) {

        // 设置请求头
        for (java.util.Map.Entry<String, String> entry : header.entrySet()) {
            httpURLConnection.setRequestProperty(entry.getKey(), entry.getValue());
        }

    }

    /**
     * todo 设置表单传输入参
     * @param ds 输出流对象

     * @return 设置入参之后的http连接对象
     */
    public void setParamFormData(DataOutputStream ds,
                         Map<String, Object> param,
                         String boundary,
                         String charset,
                         String sameName) throws IOException {
        if (null == param) {
            return;
        }

        //是否存在文件入参
        boolean isParamFile = false;

        // 普通map
        Map<String,Object> paramMap = new HashMap(HttpToolConstant.NUM_INTEGER_SIXTEEN);
        // 附件map
        Map<String,File> paramFiles = new HashMap(HttpToolConstant.NUM_INTEGER_SIXTEEN);
        for (Map.Entry<String, Object> entry : param.entrySet()) {
            // 校验是否是File类型
            if (entry.getValue() instanceof File) {
                isParamFile = true;
                paramFiles.put(entry.getKey(),(File) entry.getValue());
            } else {
                paramMap.put(entry.getKey(),entry.getValue());
            }
        }

        // 遍历普通入参
        StringBuilder sb = new StringBuilder();
        for (java.util.Map.Entry<String, Object> entry : paramMap.entrySet()) {
            sb.append(HttpToolConstant.PREFIX);
            sb.append(boundary);
            sb.append(HttpToolConstant.LINEND);
            sb.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"" + HttpToolConstant.LINEND);
            sb.append("Content-Type: text/plain; charset=" + charset + HttpToolConstant.LINEND);
            /*
                在用传输文件时，设置MultiPart之后，传输的内容会自动添加Content-Transfer-Encoding:binary的头域。
                这个head域是用来描述内容在传输过程中的编码格式。这个域不是必须的。
                仅定义一种Content-Transfer-Encoding也是不可以的。
                Content-Transfer-Encoding支持以下数据格式：BASE64, QUOTED-PRINTABLE, 8BIT, 7BIT, BINARY, X-TOKEN。
                这些值是大小写不敏感的。7BIT是默认值。
                当不设置Content-Transfer-Encoding的时候，默认就是7BIT。7BIT的含义是所有的数据以ASC-II格式的格式编码，
                8BIT则可能包含非ASCII字符。BINARY可能不止包含非ASCII字符，还可能不是一个短行（超过1000字符）。
             */
            sb.append("Content-Transfer-Encoding: 8bit" + HttpToolConstant.LINEND);
            sb.append(HttpToolConstant.LINEND);
            sb.append(entry.getValue().toString());
            sb.append(HttpToolConstant.LINEND);
        }
        //写入流
        ds.write(sb.toString().getBytes(charset));
        if (isParamFile) {
            // 遍历要传输的文件
            for (java.util.Map.Entry<String, File> entry : paramFiles.entrySet()) {
                // 设置附件同名还是不同名
                String name = sameName == null ? entry.getKey() : sameName;
                // 填写文件传输前缀
                ds.write((HttpToolConstant.PREFIX + boundary + HttpToolConstant.LINEND).getBytes(charset));
                ds.write(("Content-Disposition: form-data; " + "name=\"" + name
                        + "\";filename=\"" + entry.getValue().getName()
                        + "\"" + HttpToolConstant.LINEND).getBytes(charset));
                ds.write(HttpToolConstant.LINEND.getBytes(charset));
                // 获取文件输入流
                FileInputStream fStream = new FileInputStream(entry.getValue());
                byte[] buffer = new byte[HttpToolConstant.NUM_INTEGER_KB];
                int length = -1;
                while ((length = fStream.read(buffer)) != -1) {
                    ds.write(buffer, 0, length);
                }
                ds.write(HttpToolConstant.LINEND.getBytes(charset));
                // 关闭输入流
                fStream.close();
            }
        }
        ds.write((HttpToolConstant.PREFIX + boundary + HttpToolConstant.PREFIX + HttpToolConstant.LINEND).getBytes(charset));

    }


    /**
     * todo 请求返回处理
     * @param httpURLConnection http连接
     * @return 返回结果
     * @throws IOException 输入输出流异常
     * @throws HttpConnectException http连接异常
     */
    public String handleResponse(HttpURLConnection httpURLConnection, boolean text, String charset) throws IOException, HttpConnectException {

        // 获得返回值
        // 假如字节流读取有乱码可以尝试读取字符流
        if (!text) {
            InputStream is = httpURLConnection.getInputStream();
            if (is != null) {
                ByteArrayOutputStream outStream = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int len = 0;
                while ((len = is.read(buffer)) != -1) {
                    outStream.write(buffer, 0, len);
                }
                is.close();
                return   new String(outStream.toByteArray());
            }
        } else {
            BufferedReader reader;
            StringBuffer sb = new StringBuffer();
            reader = new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream(), charset));
            String lines;
            while ((lines = reader.readLine()) != null) {
                sb.append(lines);
            }
            return sb.toString();
        }
        return null;
    }

    /**
     * todo 请求错误异常信息构建
     * @param code 错误编码
     * @param httpTool http请求实体
     * @return 返回结果
     */
    public String msgBuild(Integer code, HttpTool httpTool) {
        Map<String,Object> errorMap = new HashMap(HttpToolConstant.NUM_INTEGER_SIXTEEN);
        errorMap.put("code", code);
        errorMap.put("msg", codeMsg(code));
        errorMap.put("requestBody", httpTool);
        return JSON.toJSONString(errorMap);
    }






    /**
     * todo 将map类型入参变为String类型入参
     * @param param 入参
     * @return 结果
     */
    public String mapToStringParam(Map<String, Object> param) throws UnsupportedEncodingException {
        return mapToStringParam(param, HttpToolConstant.CODE_FORMAT_UTF_8);
    }

    /**
     * 将map类型入参变为String类型入参
     * @param param 入参
     * @param charset 自定义编码
     * @return 结果
     */
    public String mapToStringParam(Map<String, Object> param,String charset) throws UnsupportedEncodingException {
        StringBuilder sbQuery = new StringBuilder();
        for (Map.Entry<String, Object> m : param.entrySet()) {
            if (null == m.getValue()){
                throw new NullPointerException("入参param值不能为null");
            }
            String value = m.getValue().toString();
            if (0 < sbQuery.length()) {
                sbQuery.append("&");
            }
            if (StringUtils.isBlank(m.getKey()) && !StringUtils.isBlank(value)) {
                sbQuery.append(m.getValue());
            }
            if (!StringUtils.isBlank(m.getKey())) {
                sbQuery.append(m.getKey());
                if (!StringUtils.isBlank(value)) {
                    sbQuery.append("=");
                    sbQuery.append(URLEncoder.encode(value, charset));
                }
            }
        }
        return sbQuery.toString();
    }


    /**
     * todo 对get请求url编码
     * @param url url参数编码
     * @return
     */
    public String getUrlEncode(String url) throws UnsupportedEncodingException {
        String[] split = url.split(HttpToolConstant.CHAR_QUSTION);
        if (split.length == 1){
            return url;
        } else {
            Map<String, Object> map = new HashMap<>(HttpToolConstant.NUM_INTEGER_SIXTEEN);
            String[] keyVals = split[1].split(HttpToolConstant.CHAR_AND);
            for (String str : keyVals) {
                String[] keyVal = str.split(HttpToolConstant.CHAR_EQUAL);
                map.put(keyVal[0],keyVal[1] == null ? null:keyVal[1]);
            }
            String s = mapToStringParam(map);
            return split[0] + "?" + s;
        }
    }

    /**
     * todo 根据返回code封装异常信息
     * @param code 请求返回状态码
     * @return 状态码对应信息
     */
    public static String codeMsg(Integer code) {
        if (code < HttpToolConstant.SC_OK) {
            // 1xx
            return code1XX(code);
        } else if (HttpToolConstant.SC_OK <= code && code < HttpToolConstant.SC_MULTIPLE_CHOICES) {
            // 2xx
            return code2XX(code);
        } else if (HttpToolConstant.SC_MULTIPLE_CHOICES <= code && code < HttpToolConstant.SC_BAD_REQUEST) {
            // 3xx
            return code3XX(code);
        } else if (HttpToolConstant.SC_BAD_REQUEST <= code && code < HttpToolConstant.SC_INTERNAL_SERVER_ERROR) {
            // 4xx
            return code4XX(code);
        } else {
            // 5xx
            return code5XX(code);
        }
    }

    /**
     * 1xx（临时响应）
     * 表示临时响应并需要请求者继续执行操作的状态代码。
     * @param code 请求返回状态码
     * @return 状态码对应信息
     */
    private static String code1XX(Integer code) {
        String res = null;
        switch (code) {
            case HttpToolConstant.SC_CONTINUE :
                res = "100 （继续） 请求者应当继续提出请求。服务器返回此代码表示已收到请求的第一部分，正在等待其余部分。";
                break;
            case HttpToolConstant.SC_SWITCHING_PROTOCOLS :
                res = "101 （切换协议） 请求者已要求服务器切换协议，服务器已确认并准备切换。";
                break;
            default:
                res = "访问异常";
        }
        return res;
    }

    /**
     * 2xx （成功）
     * 表示成功处理了请求的状态代码。
     * @param code 请求返回状态码
     * @return 状态码对应信息
     */
    private static String code2XX(Integer code) {
        String res = null;
        switch (code) {
            case HttpToolConstant.SC_OK :
                res = "200 （成功） 服务器已成功处理了请求。通常，这表示服务器提供了请求的网页。";
                break;
            case HttpToolConstant.SC_CREATED :
                res = "201 （已创建） 请求成功并且服务器创建了新的资源。";
                break;
            case HttpToolConstant.SC_ACCEPTED :
                res = "202 （已接受） 服务器已接受请求，但尚未处理。";
                break;
            case HttpToolConstant.SC_NON_AUTHORITATIVE_INFORMATION :
                res = "203 （非授权信息） 服务器已成功处理了请求，但返回的信息可能来自另一来源。";
                break;
            case HttpToolConstant.SC_NO_CONTENT :
                res = "204 （无内容） 服务器成功处理了请求，但没有返回任何内容。";
                break;
            case HttpToolConstant.SC_RESET_CONTENT :
                res = "205 （重置内容） 服务器成功处理了请求，但没有返回任何内容。";
                break;
            case HttpToolConstant.SC_PARTIAL_CONTENT :
                res = "206 （部分内容） 服务器成功处理了部分 GET 请求。";
                break;
            default:
                res = "访问异常";
        }
        return res;
    }

    /**
     * 3xx （重定向）
     * 表示要完成请求，需要进一步操作。 通常，这些状态代码用来重定向。
     * @param code 请求返回状态码
     * @return 状态码对应信息
     */
    private static String code3XX(Integer code) {
        String res = null;
        switch (code) {
            case HttpToolConstant.SC_MULTIPLE_CHOICES :
                res = "300 （多种选择） 针对请求，服务器可执行多种操作。服务器可根据请求者 (user agent) 选择一项操作，或提供操作列表供请求者选择。";
                break;
            case HttpToolConstant.SC_MOVED_PERMANENTLY :
                res = "301 （永久移动） 请求的网页已永久移动到新位置。服务器返回此响应（对 GET 或 HEAD 请求的响应）时，会自动将请求者转到新位置。";
                break;
            case HttpToolConstant.SC_MOVED_TEMPORARILY :
                res = "302 （临时移动） 服务器目前从不同位置的网页响应请求，但请求者应继续使用原有位置来进行以后的请求。";
                break;
            case HttpToolConstant.SC_SEE_OTHER :
                res = "303 （查看其他位置） 请求者应当对不同的位置使用单独的 GET 请求来检索响应时，服务器返回此代码。";
                break;
            case HttpToolConstant.SC_NOT_MODIFIED :
                res = "304 （未修改） 自从上次请求后，请求的网页未修改过。服务器返回此响应时，不会返回网页内容。";
                break;
            case HttpToolConstant.SC_USE_PROXY :
                res = "305 （使用代理） 请求者只能使用代理访问请求的网页。如果服务器返回此响应，还表示请求者应使用代理。";
                break;
            case HttpToolConstant.SC_TEMPORARY_REDIRECT :
                res = "307 （临时重定向） 服务器目前从不同位置的网页响应请求，但请求者应继续使用原有位置来进行以后的请求。";
                break;
            default:
                res = "访问异常";
        }
        return res;
    }

    /**
     * 4xx （客户端错误）
     * 客户端的请求有错误
     * @param code 请求返回状态码
     * @return 状态码对应信息
     */
    private static String code4XX(Integer code) {
        String res = null;
        switch (code) {
            case HttpToolConstant.SC_BAD_REQUEST :
                res = "400异常通常为编码问题,访问路径的接口无法解析";
                break;
            case HttpToolConstant.SC_UNAUTHORIZED :
                res = "401异常（未授权） 请求要求身份验证。 对于需要登录的网页，服务器可能返回此响应。";
                break;
            case HttpToolConstant.SC_FORBIDDEN :
                res = "403 （禁止） 服务器拒绝请求。";
                break;
            case HttpToolConstant.SC_NOT_FOUND :
                res = "404一般是接口错了，服务找不到请求网页";
                break;
            case HttpToolConstant.SC_METHOD_NOT_ALLOWED :
                res = "405异常一般是请求方式不正确，确认访问路径的接口是POST请求、GET请求还是其他请求";
                break;
            case HttpToolConstant.SC_NOT_ACCEPTABLE :
                res = "406 （不接受） 无法使用请求的内容特性响应请求的网页。";
                break;
            case HttpToolConstant.SC_PROXY_AUTHENTICATION_REQUIRED :
                res = "407异常（需要代理授权） 此状态代码与 401（未授权）类似，但指定请求者应当授权使用代理。";
                break;
            case HttpToolConstant.SC_REQUEST_TIMEOUT :
                res = "408（请求超时） 服务器等候请求时发生超时。";
                break;
            case HttpToolConstant.SC_CONFLICT :
                res = "409 （冲突） 服务器在完成请求时发生冲突。服务器必须在响应中包含有关冲突的信息。";
                break;
            case HttpToolConstant.SC_GONE :
                res = "410 （已删除） 如果请求的资源已永久删除，服务器就会返回此响应。";
                break;
            case HttpToolConstant.SC_LENGTH_REQUIRED :
                res = "411 （需要有效长度） 服务器不接受不含有效内容长度标头字段的请求。";
                break;
            case HttpToolConstant.SC_PRECONDITION_FAILED :
                res = "412 （未满足前提条件） 服务器未满足请求者在请求中设置的其中一个前提条件。";
                break;
            case HttpToolConstant.SC_REQUEST_TOO_LONG :
                res = "413 （请求实体过大） 服务器无法处理请求，因为请求实体过大，超出服务器的处理能力。";
                break;
            case HttpToolConstant.SC_REQUEST_URI_TOO_LONG :
                res = "414异常（请求的 URI 过长） 请求的 URI（通常为网址）过长，服务器无法处理。";
                break;
            case HttpToolConstant.SC_UNSUPPORTED_MEDIA_TYPE :
                res = "415 （不支持的媒体类型） 请求的格式不受请求页面的支持。";
                break;
            case HttpToolConstant.SC_REQUESTED_RANGE_NOT_SATISFIABLE :
                res = "416 （请求范围不符合要求） 如果页面无法提供请求的范围，则服务器会返回此状态代码。";
                break;
            case HttpToolConstant.SC_EXPECTATION_FAILED :
                res = "417 （未满足期望值） 服务器未满足”期望”请求标头字段的要求。";
                break;
            default:
                res = "访问异常";
        }
        return res;
    }

    /**
     * 5xx（服务器错误）
     * 这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误，而不是请求出错。
     * @param code 请求返回状态码
     * @return 状态码对应信息
     */
    private static String code5XX(Integer code) {
        String res = null;
        switch (code) {
            case HttpToolConstant.SC_INTERNAL_SERVER_ERROR :
                res = "500 （服务器内部错误） 服务器遇到错误，无法完成请求。";
                break;
            case HttpToolConstant.SC_NOT_IMPLEMENTED :
                res = "501 （尚未实施） 服务器不具备完成请求的功能。例如，服务器无法识别请求方法时可能会返回此代码。";
                break;
            case HttpToolConstant.SC_BAD_GATEWAY :
                res = "502 （错误网关） 服务器作为网关或代理，从上游服务器收到无效响应。";
                break;
            case HttpToolConstant.SC_SERVICE_UNAVAILABLE :
                res = "503 （服务不可用） 服务器目前无法使用（由于超载或停机维护）。通常，这只是暂时状态。";
                break;
            case HttpToolConstant.SC_GATEWAY_TIMEOUT :
                res = "504 （网关超时） 服务器作为网关或代理，但是没有及时从上游服务器收到请求。";
                break;
            case HttpToolConstant.SC_HTTP_VERSION_NOT_SUPPORTED :
                res = "505 （HTTP 版本不受支持） 服务器不支持请求中所用的 HTTP 协议版本。";
                break;
            default:
                res = "访问异常";
        }
        return res;
    }
}
